/* @flow */

import config from '../config';
import VNode, { createEmptyVNode } from './vnode';
import { createComponent } from './create-component';
import { traverse } from '../observer/traverse';

import { warn, isDef, isUndef, isTrue, isObject, isPrimitive, resolveAsset } from '../util/index';

import { normalizeChildren, simpleNormalizeChildren } from './helpers/index';

const SIMPLE_NORMALIZE = 1;
const ALWAYS_NORMALIZE = 2;

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
// tag:string标签或组件选择对象
// data:标签属性、dom元素属性、或注册事件
// children:string/array。string:标签内内容,array:标签内子节点
// normalizationType:
// alwaysNormalize:
export function createElement(
    context: Component,
    tag: any,
    data: any,
    children: any,
    normalizationType: any,
    alwaysNormalize: boolean
): VNode | Array<VNode> {
    // data是数组或者是原始值。数组：子节点，原始值：标签之间内容
    if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children;
        children = data;
        data = undefined; //data未传入。data赋值给children
    }
    // 如果调用用户传入的render，会将值normalizationType设为2，用来处理children参数
    if (isTrue(alwaysNormalize)) {
        normalizationType = ALWAYS_NORMALIZE;
    }
    return _createElement(context, tag, data, children, normalizationType);
}

export function _createElement(
    context: Component,
    tag?: string | Class<Component> | Function | Object,
    data?: VNodeData,
    children?: any,
    normalizationType?: number
): VNode | Array<VNode> {
    if (isDef(data) && isDef((data: any).__ob__)) {
        process.env.NODE_ENV !== 'production' &&
            warn(
                `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
                    'Always create fresh vnode data objects in each render!',
                context
            );
        return createEmptyVNode();
    }
    // object syntax in v-bind
    if (isDef(data) && isDef(data.is)) {
        tag = data.is;
    }
    if (!tag) {
        // in case of component :is set to falsy value
        return createEmptyVNode();
    }
    // warn against non-primitive key
    if (
        process.env.NODE_ENV !== 'production' &&
        isDef(data) &&
        isDef(data.key) &&
        !isPrimitive(data.key)
    ) {
        if (!__WEEX__ || !('@binding' in data.key)) {
            warn(
                'Avoid using non-primitive value as key, ' + 'use string/number value instead.',
                context
            );
        }
    }
    // support single function children as default scoped slot
    if (Array.isArray(children) && typeof children[0] === 'function') {
        data = data || {};
        data.scopedSlots = { default: children[0] };
        children.length = 0;
    }
    if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children);
    } else if (normalizationType === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children);
    }
    let vnode, ns;
    if (typeof tag === 'string') {
        let Ctor;
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
        if (config.isReservedTag(tag)) {
            // platform built-in elements
            if (
                process.env.NODE_ENV !== 'production' &&
                isDef(data) &&
                isDef(data.nativeOn) &&
                data.tag !== 'component'
            ) {
                warn(
                    `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
                    context
                );
            }
            vnode = new VNode(
                config.parsePlatformTagName(tag),
                data,
                children,
                undefined,
                undefined,
                context
            );
        } else if (
            (!data || !data.pre) &&
            isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
        ) {
            // component
            vnode = createComponent(Ctor, data, context, children, tag);
        } else {
            // unknown or unlisted namespaced elements
            // check at runtime because it may get assigned a namespace when its
            // parent normalizes children
            vnode = new VNode(tag, data, children, undefined, undefined, context);
        }
    } else {
        // direct component options / constructor
        vnode = createComponent(tag, data, context, children);
    }
    if (Array.isArray(vnode)) {
        return vnode;
    } else if (isDef(vnode)) {
        if (isDef(ns)) applyNS(vnode, ns);
        if (isDef(data)) registerDeepBindings(data);
        return vnode;
    } else {
        return createEmptyVNode();
    }
}

function applyNS(vnode, ns, force) {
    vnode.ns = ns;
    if (vnode.tag === 'foreignObject') {
        // use default namespace inside foreignObject
        ns = undefined;
        force = true;
    }
    if (isDef(vnode.children)) {
        for (let i = 0, l = vnode.children.length; i < l; i++) {
            const child = vnode.children[i];
            if (isDef(child.tag) && (isUndef(child.ns) || (isTrue(force) && child.tag !== 'svg'))) {
                applyNS(child, ns, force);
            }
        }
    }
}

// ref #5318
// necessary to ensure parent re-render when deep bindings like :style and
// :class are used on slot nodes
function registerDeepBindings(data) {
    if (isObject(data.style)) {
        traverse(data.style);
    }
    if (isObject(data.class)) {
        traverse(data.class);
    }
}
